package com.neusoft.databus.agent.parser.modbus;

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;

import org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.JexlException;
import org.apache.commons.jexl3.MapContext;
import org.springframework.util.StringUtils;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.neusoft.databus.agent.bean.MetricConfig;
import com.neusoft.databus.common.bean.CommandBean;
import com.neusoft.databus.common.bean.MetricBean;
import com.neusoft.databus.common.constant.DatabusConstant;
import com.neusoft.databus.common.enums.MetricValueType;
import com.neusoft.databus.common.exception.ConfigErrorException;
import com.neusoft.databus.common.parser.Parser;
import com.neusoft.databus.common.utils.DigitalUtils;
import com.neusoft.databus.common.utils.JexlMath;
import com.neusoft.databus.common.utils.JexlUtil;

import lombok.extern.slf4j.Slf4j;

/**
 * Modbus parser
 *
 * @author sunchf
 * @date 2018年12月28日 下午3:07:45
 */
@Slf4j
public abstract class ModBusParser implements Parser {

    public static final byte[] ERROR_FUNCTION_CODE = new byte[] {};

    /**
     * @param commandBean CommandBean
     * @return ADU:List<MetricBean>
     */
    @Override
    public Multimap<String, MetricBean> parseCollectRequest(final CommandBean commandBean) {
        final Multimap<String, MetricBean> multimap = ArrayListMultimap.create();
        commandBean.getResourceBean().forEach(it -> {
            final Map<String, Object> config = it.getConfig();
            if (!config.containsKey(DatabusConstant.SLAVE)) {
                throw new ConfigErrorException("Modbus设备未配置slave项");
            }
            final int slave = Integer.valueOf(String.valueOf(config.get(DatabusConstant.SLAVE)));

            for (final MetricBean metricBean : it.getMetrics()) {
                final String req = DigitalUtils
                        .bytesToHexString(ModBusParser.this.generateReq(String.format("%02x", slave), metricBean));
                if (null != req) {
                    multimap.put(req, metricBean);
                }
            }
        });

        return multimap;
    }

    @Override
    public void parseCollectResponse(final CommandBean commandBean) {
        commandBean.getResourceBean().forEach(it -> {
            final Map<String, byte[]> metricCodeAndCollectedBytesMap = Maps.newHashMap();

            it.getMetrics().stream().filter(data -> data.isExecutedSuccess())
                    .forEach(data -> metricCodeAndCollectedBytesMap.put(data.getVariable(),
                            (byte[]) data.getOriginalValue()));

            // 以资源为单位设置表达式环境变量
            final JexlContext jexlContext = new MapContext();
            jexlContext.set("Math", JexlMath.getInstance());
            // 计算原值
            for (final MetricBean metricBean1 : it.getMetrics()) {
                if (ModBusParser.log.isDebugEnabled()) {
                    ModBusParser.log.debug("开始解析指标值，{}, {}", metricBean1.getRule(),
                            DigitalUtils.bytesToHexString((byte[]) metricBean1.getOriginalValue()));
                }
                final String metricVariable = metricBean1.getVariable();
                final byte[] metircByteValue = metricCodeAndCollectedBytesMap.get(metricVariable);

                if (!ModBusParser.this.validateResult(metircByteValue)) {
                    continue;
                }
                if (ModBusParser.this.isErrorFunctionCode(metircByteValue)) {
                    continue;
                }

                try {
                    final byte[] resultArray = ModBusParser.this.extractBusinessData(metircByteValue);
                    if (ModBusParser.log.isDebugEnabled()) {
                        ModBusParser.log.debug("指标值解析目标 {}", DigitalUtils.bytesToHexString(resultArray));
                    }
                    final MetricConfig metricConfig1 = MetricConfig.create(metricBean1.getRule());
                    Object value1 = ModBusParser.this.bytesToValueRealOffset(metricBean1.getValueType(), resultArray,
                            metricConfig1);

                    if (ModBusParser.log.isDebugEnabled()) {
                        ModBusParser.log.debug("解析值 {}，类型 {}", value1, value1.getClass().getSimpleName());
                    }
                    if (value1 instanceof Number) {
                        jexlContext.set(metricVariable, ((Number) value1).doubleValue());
                        if (ModBusParser.log.isDebugEnabled()) {
                            ModBusParser.log.debug("变量 {}，值 {}", metricVariable, value1);
                        }
                    }

                    if ((value1 instanceof Double) || (value1 instanceof Float)) {
                        final BigDecimal decimal1 = new BigDecimal(value1.toString());
                        value1 = decimal1.setScale(metricConfig1.getPrecision(), BigDecimal.ROUND_HALF_DOWN)
                                .doubleValue();
                    }
                    metricBean1.setCollectedTime(new Date());
                    // if (!commandBean.isForTest()) {
                    //     metricBean1.setOriginalValue(null);
                    // } else {
                    metricBean1.setOriginalValue(DigitalUtils.bytesToHexString(metircByteValue));
                    // }
                    metricBean1.setValue(value1);
                } catch (final Exception e) {
                    ModBusParser.log.error("指标值解析失败,指标名:{},指标code:{},解析目标{},失败原因: {}", metricBean1.getName(),
                            metricVariable,
                            DigitalUtils.bytesToHexString(metricCodeAndCollectedBytesMap.get(metricVariable)),
                            e.getMessage());
                    ModBusParser.log.error("", e);
                    continue;
                }
            }

            // 计算表达是值
            for (final MetricBean metricBean2 : it.getMetrics()) {
                try {
                    final MetricConfig metricConfig2 = MetricConfig.create(metricBean2.getRule());
                    if (!StringUtils.isEmpty(metricConfig2.getExpression())) {
                        if (ModBusParser.log.isDebugEnabled()) {
                            ModBusParser.log.debug("指标存在表达式 {}。", metricConfig2.getExpression());
                        }
                        try {
                            Object value2 = JexlUtil.evaluate(metricConfig2.getExpression(), jexlContext);
                            if ((value2 instanceof Double) || (value2 instanceof Float)) {
                                final BigDecimal decimal2 = new BigDecimal(value2.toString());
                                value2 = decimal2.setScale(metricConfig2.getPrecision(), BigDecimal.ROUND_HALF_DOWN)
                                        .doubleValue();
                            }
                            metricBean2.setValue(value2);
                        } catch (final JexlException e) {
                            ModBusParser.log.error(e.getMessage());
                        }
                    }
                } catch (final Exception e) {
                    ModBusParser.log.error(e.getMessage(), e);
                }
            }

        });

    }

    /**
     * data transfer by value type definition and configuration
     *
     * @param valueType declared value type
     * @param data extracted business data bytes
     * @param metricConfig metric extended configuration
     * @return
     */
    private Number bytesToValueRealOffset(final MetricValueType valueType, final byte[] data,
            final MetricConfig metricConfig) {
        final int offset = metricConfig.getStart();
        Number number = 0;
        switch (valueType) {
        case BINARY:
            Integer bitStart = metricConfig.getBitStart();
            if (null == bitStart) {
                bitStart = 0;
            }
            Integer bitLength = metricConfig.getBitLength();
            if (null == bitLength) {
                bitLength = 1;
            }
            final int affectedByteLength = (((bitStart + bitLength) - 1) / 8) + 1;
            String binaryStr = "";
            for (int i = 0; i < affectedByteLength; i++) {
                binaryStr += Integer.toBinaryString((data[offset + i] & 0xFF) + 0x100).substring(1);
            }

            final String partial = binaryStr.substring(bitStart, bitStart + bitLength);
            number = Integer.valueOf(partial, 2);
            break;
        // 1 byte
        case BYTE:
            number = new Integer(data[offset] & 0xff);
            break;
        // 2 bytes
        case USHORT:
            number = DigitalUtils.bytesToUShort(data, offset);
            break;
        case SHORT:
            number = DigitalUtils.bytesToShort(data, offset);
            break;
        case USHORT_SWAPPED:
            number = DigitalUtils.bytesToUShortSwapped(data, offset);
            break;
        case SHORT_SWAPPED:
            number = DigitalUtils.bytesToShortSwapped(data, offset);
            break;
        // 4 bytes
        case UINT:
            number = DigitalUtils.bytesToUInt(data, offset);
            break;
        case INT:
            number = DigitalUtils.bytesToInt(data, offset);
            break;
        case UINT_SWAPPED:
            number = DigitalUtils.bytesToUIntSwapped(data, offset);
            break;
        case INT_SWAPPED:
            number = DigitalUtils.bytesToIntSwapped(data, offset);
            break;
        case UINT_SWAPPED_SWAPPED:
            number = DigitalUtils.bytesToUIntSwappedSwapped(data, offset);
            break;
        case INT_SWAPPED_SWAPPED:
            number = DigitalUtils.bytesToIntSwappedSwapped(data, offset);
            break;
        case FLOAT:
            number = DigitalUtils.bytesToFloat(data, offset);
            break;
        case FLOAT_SWAPPED:
            number = DigitalUtils.bytesToFloatSwapped(data, offset);
            break;
        // 8 bytes
        case ULONG:
            number = DigitalUtils.bytesToULong(data, offset);
            break;
        case LONG:
            number = DigitalUtils.bytesToLong(data, offset);
            break;
        case ULONG_SWAPPED:
            number = DigitalUtils.bytesToULongSwapped(data, offset);
            break;
        case LONG_SWAPPED:
            number = DigitalUtils.bytesToLongSwapped(data, offset);
            break;
        case DOUBLE:
            number = DigitalUtils.bytesToDouble(data, offset);
            break;
        case DOUBLE_SWAPPED:
            number = DigitalUtils.bytesToDoubleSwapped(data, offset);
            break;
        default:
            break;
        }

        return number;
    }

    /**
     * extract business data bytes(register data)
     *
     * @param packet the whole response bytes
     * @return
     */
    public byte[] extractBusinessData(final byte[] packet) {
        int realDataLengthByteOffset = this.getResponseHeaderLength() - 1;
        if (packet[realDataLengthByteOffset] == 0) {
            realDataLengthByteOffset = this.getResponseHeaderLength();
        }
        final int dataLength = DigitalUtils.bytesToUShort(new byte[] {packet[realDataLengthByteOffset] }, 0);

        final byte[] pdu = Arrays.copyOfRange(packet, realDataLengthByteOffset + 1,
                realDataLengthByteOffset + 1 + dataLength);
        return pdu;
    }

    /**
     * get the header length of response. It's protocol concern. It's logical
     * declaration, not standard definition, for simplicity of parse response
     * data .<br >
     * RTU: 3. slave(1)、function code(1)、business data length(1) <br >
     * TCP: 9. transaction id(2), protocol id(2)、data length(include function
     * code and business data length)(2)、slave(1)、 function code(1)、business
     * data length(1) <br >
     * ASCII: 4. start identifier(1)、slave(1)、function code(1)、business data
     * length(1) <br >
     *
     * @return
     */
    public abstract int getResponseHeaderLength();

    /**
     * check function code of response data.
     *
     * @param packet the packet of response data
     * @return
     */
    public boolean isErrorFunctionCode(final byte[] packet) {

        final int functionCodeIdx = this.getResponseHeaderLength() - 2;
        if ((packet.length - 1) < functionCodeIdx) {
            return false;
        }
        final byte functionCode = packet[functionCodeIdx];

        if (functionCode >= 0x81) {
            if (ModBusParser.log.isWarnEnabled()) {
                ModBusParser.log.warn("error function code {}", functionCode);
            }
            return true;
        }
        return false;
    }

    /**
     * generate read request bytes by device slave id and metric command
     * configuration
     *
     * @param slave slave id
     * @param metricBean metric definition bean
     * @return
     */
    public abstract byte[] generateReq(String slave, MetricBean metricBean);

    /**
     * serial input stream can not detect end of file, so use
     * expectedResDataLength as a condition to decide end of input stream. <br>
     * if expectedResDataLength is set to Integer.MAX_VALUE, it will not be
     * used;
     *
     * @param req
     * @return
     */
    public abstract int expectedResDataLength(byte[] req);

    /**
     * validate the response data of read request
     *
     * @param packet
     * @return
     */
    public abstract boolean validateResult(byte[] packet);

}
